perf(unified-model): per-track micro-batch geometry for AR and image - #301
Open
leviking98z-rgb wants to merge 1 commit into
Open
Conversation
The unified-model stack slices both lineage levels with one shared ``micro_batch_size``, but the two tracks are shaped differently: AR rows are variable-length responses whose activation peak is set by the longest row in a micro, while image rows are fixed-shape latents where a larger micro is clean parallelism. One number cannot suit both, so the image track is pinned to whatever the AR track can afford. Add optional ``ar_micro_batch_size`` / ``image_micro_batch_size``, both defaulting to the shared ``micro_batch_size`` so omitting them is a no-op. ``_optimizer_step_slices`` and ``prepare_segment`` take the size explicitly, which keeps a replay-anchored track's π_old anchor frozen at exactly the geometry training will use (an anchor recomputed at a different micro size would break the on-policy ratio). Also weight each micro's metrics by its sample share in ``_backward_part``, so a ragged final micro no longer skews the reported mean toward its fewer rows. This matches the sample-share ``loss_scale`` already used for backward; equal micros reduce to the previous plain average. ``aggregate_numeric_metrics`` grows a keyword-only ``weights`` argument, renormalized per key so a metric present in only some micros still averages correctly.
leviking98z-rgb
force-pushed
the
codex/independent-track-dp-scatter
branch
from
August 3, 2026 15:44
133c11c to
6cd3c09
Compare
leviking98z-rgb
marked this pull request as ready for review
August 4, 2026 03:25
leviking98z-rgb
requested review from
Ideny42,
haonan3 and
zzhuoxin1508
as code owners
August 4, 2026 03:25
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
UnifiedModelTrainStackslices both lineage levels with one sharedmicro_batch_size, but the two tracks are shaped differently:One number cannot suit both, so today the image track is pinned to whatever the AR track can afford.
This adds optional
ar_micro_batch_size/image_micro_batch_size. Both default to the sharedmicro_batch_size, so omitting them is a behavioral no-op._optimizer_step_slicesandprepare_segmentnow take the size explicitly, which matters for correctness: a replay-anchored track (FlowGRPO underold_logp_source='replay') recomputes its π_old anchor at the exact(mini, micro)slices training will use, so the anchor must be frozen at the same per-track micro size or the on-policy ratio stops being 1.Second, smaller change:
_backward_partweights each micro's metrics by its sample share. The backward pass already uses a sample-shareloss_scale, but metrics were averaged unweighted, so a ragged final micro (batch not divisible by the micro size) skewed the reported mean toward its fewer rows.aggregate_numeric_metricsgrows a keyword-onlyweightsargument, renormalized per key so a metric present in only some micros still averages correctly. Equal-sized micros reduce to the previous plain average.Loss-equivalence caveat (documented in code): micro-batching only reconstructs the whole-batch objective when the per-micro sample share is the right weight. That holds for a per-row mean — FlowGRPO's flat latent mean, and GRPO's
seq-mean-*modes — but not for GRPO's defaulttoken-meanwhen responses differ in length, where the exact objective weights by token count rather than row count. Soar_micro_batch_size > 1wants aseq-mean-*loss_agg_mode. The image track (FlowGRPO) has no such constraint. This is why the recipes ship both knobs asnullrather than raising the image default here.Related Issue
N/A
Test Plan
All commands run from the repo root at
133c11c, venv with torch 2.13.0+cu130.1. Behavior harness (24 assertions; uncommitted per the
tests/-removal policy, source quoted below):Covers: defaults reproduce the shared value;
_optimizer_step_slicesomitting the new kwarg is identical to passing the shared size; AR=8@mbs1 → 8 micros while image=16@mbs2 → 8 micros of width 2, each covering all rows; ragged 5@mbs2 →[(0,2),(2,4),(4,5)]with shares summing to 1 and the ragged share strictly smaller;num_updates=2splits before micro-batching into disjoint ordered mini-batches; non-positive sizes rejected; and foraggregate_numeric_metrics— unweighted path unchanged, equal weights == plain mean, ragged weights give 3.0 where the unweighted mean gives 2.5, per-key renormalization when a key is missing, length mismatch raises, zero total weight falls back, bool/tensor coercion preserved.2. Recipe keys resolve against the constructor:
3. Repo gates:
Not run: multi-GPU training A/B on this branch. See Reviewer Notes — the measurement I have is from a different base and I am not presenting it as a result for this diff.
Compatibility / Risk
micro_batch_size; the three shipped recipes set them tonull._optimizer_step_slices/prepare_segmentkeep their old call form working (the new argument is keyword-only with aNonedefault).aggregate_numeric_metrics(weights=...)is keyword-only and defaults toNone; the three existing callers (diffusionnft.py,train/stack/base.py×2) are untouched and unaffected.ar_micro_batch_sizeabove 1 shifts the AR objective underloss_agg_mode: token-mean(see Summary). Left at the default, nothing shifts.Reviewer Notes
Verification: every line is mine to defend; the diff and all commands above were reviewed and run.
Duplicate-work check: scanned the 33 open PRs. Nothing else touches
unirl/train/unified_model_stack.pyorunirl/utils/misc.py. #299 (share single-stream sampling/replay loops) and #156 (batched-step replay to more models) are in the diffusion-replay area and do not overlap this change; #253 (batch trainside UniGRPO rollout forwards) touchesbagel/*model code plus the same recipe file, but a different section — worth a glance for recipe conflict, not for logic.On the performance claim — please read before merging. I measured
image_micro_batch_size=2at roughly −20% train-step time on 8×H20 HI3 (batch_size 8, 4 diffusion steps / 2 SDE, 512px, 1 step), at the cost of ~5 GB more peak allocated. But that run sat on a different, older base with a different fix for the DP-sharding problem — I had independently hit the "image Part gets replicated to every rank" bug and solved it with a new per-argumentDP_SCATTER_INDEPENDENTdispatch mode, before finding thatmainalready fixes it (and better) by passing the whole lineageSampleand tree-sharding it. So:Happy to re-measure if someone can point me at capacity, and equally happy for this to sit as draft until then. The code is correctness-complete and default-inert regardless; the open question is purely how much the knob buys on current
main.Suggested review order: the
prepare_segmentanchor-geometry coupling is the only subtle part — if a replay-anchored track's anchor were frozen at a different micro size than training uses, the ratio would silently stop being 1. Everything else is plumbing.